// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-07-19

package src

import (
	"context"
	"fmt"
	"sort"
	"strings"
)

const (
	builtinHelpName                  = "help"
	builtinHelpOptCommand            = "command"
	builtinHelpOptCommandShort       = 'c'
	builtinHelpOptCommandDescription = "Which command to be showed"
	builtinHelpDescription           = "A self-sufficient runtime for console"
)

// Help
// is a built-in command provider used to print usages about console and command.
type Help struct {
	self bool
	show *Command
}

// +---------------------------------------------------------------------------+
// | Implements methods                                                        |
// +---------------------------------------------------------------------------+

// After
// is a handler used to call in Command.
func (o *Help) After(_ context.Context, container *Container, _ *Command, err error) {
	if err == nil && o.self {
		container.GetOutput().Info("\nRun 'go run main.go -c <COMMAND>' for more information on a command.")
	}
}

// Before
// is a handler used to call in Command.
func (o *Help) Before(_ context.Context, container *Container, command *Command) (err error) {
	// Use
	// current command.
	o.show = command

	// Use
	// specified command.
	if opt, has := command.GetOption(builtinHelpOptCommand); has {
		if name := opt.ToString(); name != "" {
			if cmd, ok := container.GetCommand(name); ok {
				o.show = cmd
			} else {
				err = fmt.Errorf("command not found: %s", name)
				return
			}
		}
	}

	// Self
	// state compared with.
	o.self = o.show.name == builtinHelpName

	// Print
	// usage of a command.
	if o.self {
		// Any command.
		container.GetOutput().Info("\nUsage: %s <COMMAND> [OPTIONS]", container.argument.GetScriptExecutor())
	} else {
		// Current command.
		container.GetOutput().Info("\nUsage: %s %s [OPTIONS]", container.argument.GetScriptExecutor(), o.show.name)
	}

	// Print
	// description of a command.
	if desc := o.show.description; desc != "" {
		for _, s := range strings.Split(desc, "\n") {
			if s = strings.TrimSpace(s); s != "" {
				container.GetOutput().Info("\n%s", s)
			}
		}
	}
	return
}

// Run
// is a handler used to call in Command.
func (o *Help) Run(_ context.Context, container *Container, command *Command) (err error) {
	// Print
	// options list of a command.
	o.printOptions(container, command)

	// Print
	// commands list of the container.
	if o.self {
		o.printCommands(container, command)
	}
	return
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

// PrintCommands
// print command list and guides.
func (o *Help) printCommands(container *Container, _ *Command) {
	var (
		desc   = make(map[string][]string)
		format string
		list   = make([]string, 0)
		mapper = container.GetCommands()
		width  int
	)

	for key, cmd := range mapper {
		if cmd.name == builtinHelpName {
			continue
		}
		list = append(list, key)
		desc[key] = cmd.formatDescription()
		if w := len(key); width < w {
			width = w
		}
	}

	if len(list) == 0 {
		return
	}

	format = fmt.Sprintf(`  %%-%ds    %%s`, width)
	container.GetOutput().Info("\nCommands:")

	// Iterate
	// option with name sorted.
	sort.Strings(list)
	for _, key := range list {
		i := 0
		// Description with mul-line.
		for _, s := range desc[key] {
			if i++; i == 1 {
				// With option.
				container.GetOutput().Info(format, key, s)
			} else {
				// Without option.
				container.GetOutput().Info(format, " ", s)
			}
		}
		// Description not specified.
		if i == 0 {
			container.GetOutput().Info(format, key, "")
		}
	}
}

// PrintOptions
// print option list and guides.
func (o *Help) printOptions(container *Container, _ *Command) {
	var (
		desc     = make(map[string][]string)
		format   string
		keys     = make(map[string]string, 0)
		list     = make([]string, 0)
		mapper   = o.show.GetOptions()
		required = make(map[string]string, 0)
		width    int
	)

	// Iterate mapper for options.
	for key, opt := range mapper {
		list = append(list, key)
		desc[key] = opt.formatDescription()
		keys[key] = opt.formatOption()

		if opt.required {
			required[key] = "*"
		} else {
			required[key] = " "
		}

		// Options width.
		if w := len(keys[key]); width < w {
			width = w
		}
	}

	// No option in command.
	if len(list) == 0 {
		return
	}

	format = fmt.Sprintf(`  %%-%ds    %%s %%s`, width)
	container.GetOutput().Info("\nOptions:")

	// Iterate
	// option with name sorted.
	sort.Strings(list)
	for _, key := range list {
		i := 0
		// Description with mul-line.
		for _, s := range desc[key] {
			if i++; i == 1 {
				// With option.
				container.GetOutput().Info(format, keys[key], required[key], s)
			} else {
				// Without option.
				container.GetOutput().Info(format, " ", " ", s)
			}
		}
		// Description not specified.
		if i == 0 {
			container.GetOutput().Info(format, keys[key], required[key], "")
		}
	}
}

// NewHelp
// creates a built-in provider.
func NewHelp() *Command {
	var (
		cmd  *Command
		help = &Help{}
	)

	cmd = NewCommand(builtinHelpName)
	cmd.defaulter = true
	cmd.description = builtinHelpDescription

	// Add options.
	cmd.Add(
		// With command.
		//
		//   -c name
		//   --command name
		//   --command=name
		//   --command="name"
		NewOption(builtinHelpOptCommand).
			SetShortName(builtinHelpOptCommandShort).
			SetDescription(builtinHelpOptCommandDescription),
	)

	return cmd.SetProvider(help)
}
